package socket;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 聊天室服务端
 */
public class Server {
    /*
      java.net.ServerSocket
      运行在服务端，主要作用有两个:
      1.向系统申请服务端口，客户端可以通过该端口与服务端建立连接
      2.监听服务端口，一旦一个客户端连接了，就会立即建立一个Socket实例与该客户端交互
     */
    private ServerSocket serverSocket; //总机
    //存放所有客户端的输出流(key:客户端的昵称 value:客户端的输出流)
    private Map<String, PrintWriter> allOut = new HashMap<>();

    public Server() {
        try {
            System.out.println("正在启动服务器");
            /*
              创建ServerSocket对象时，会申请一个端口
              如果该端口被其他程序占用，则会抛出异常:
               java.net.BindException: Address already in use
              解决办法:
              1.杀死占用该端口的进程(通常发生在服务端启动两次导致)
              2.更换端口号
             */
            serverSocket = new ServerSocket(8088);
            System.out.println("服务器启动完毕");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 该方法用于启动程序的执行
     */
    public void start() {
        try {
            while(true) {
                System.out.println("等待客户端连接...");
                    /*
                      Socket accept():
                      用于接收客户端的连接，并返回一个Scoket实例与连接的客户端交互
                      该方法是一个阻塞方法，调用后程序会"卡住"，直到一个客户端连接为止
                     */
                Socket socket = serverSocket.accept();
                System.out.println("一个客户端连接了!");
                //启动一个线程来处理该客户端的交互
                ClientHandler handler = new ClientHandler(socket);
                Thread t = new Thread(handler);
                t.start();
            }
        } catch (IOException e) {

        }
    }

    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }

    /**
     * 该线程任务用于负责与指定的客户端交互
     */
    private class ClientHandler implements Runnable {
        private Socket socket;
        private String host; //记录该客户端的IP地址
        private String nickName; //记录客户端昵称

        public ClientHandler(Socket socket) {
            this.socket = socket;
            host = socket.getInetAddress().getHostAddress(); //通过socket获取远端(客户端)计算机地址
        }

        @Override
        public void run() {
            PrintWriter pw = null;
            try {
                //通过socket获取输入流用于读取客户端发送过来的消息
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr);

                //通过socket获取输出流用于向客户端发送消息
                OutputStream out = socket.getOutputStream();
                OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);
                BufferedWriter bw = new BufferedWriter(osw);
                pw = new PrintWriter(bw, true);

                nickName = br.readLine(); //读取客户端发送过来的昵称
                synchronized (allOut) { //向集合中添加元素需要添加同步机制, 保证多个线程不能同时添加
                    allOut.put(nickName, pw); //将该客户端的昵称和输出流存入Map中
                }
                sendMessageToAll("上线了, 当前在线人数:" + allOut.size()); //通知所有客户端, 该用户上线了

                String message;
                while((message = br.readLine()) != null) { //读取客户端发送过来的消息
                    if (message.startsWith("@")) { //如果聊天内容以"@"开头则表示为私聊
                        sendMessageToSomeone(message); //调用私聊方法实现私聊
                    } else {
                        sendMessageToAll("说:" + message); //调用群聊方法实现群聊
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                //处理客户端下线后的操作, 共3步:
                synchronized (allOut) { //从集合中删除元素需要同步机制, 保证多个线程不能同时删除
                    allOut.remove(nickName); //1)根据昵称, 将该客户端的输出流从共享集合中删除
                }
                sendMessageToAll("下线了, 当前在线人数" + allOut.size()); //2)通过所有客户端, 该用户下线了
                try {
                    socket.close(); //3)关闭socket
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        /**
         * 将消息发送给所有客户端(群聊)
         * @param message 客户端发送过来的消息
         */
        private void sendMessageToAll(String message) {
            System.out.println(nickName + "[" + host + "]" + message);
            synchronized (allOut) { //群发时也要保证多个线程同步处理
                /*
                  新循环遍历集合就是迭代器, 因为迭代器要求遍历过程中不可以用集合的方式来增删元素,
                  因此为了保证遍历正确, 防止其他线程增删, 这里要和其他线程的增删互斥
                 */
                for (PrintWriter o : allOut.values()) {
                    o.println(nickName + "[" + host + "]" + message);
                }
            }
        }

        /**
         * 将消息发送给指定用户(私聊)
         * @param message
         */
        private void sendMessageToSomeone(String message) {
            //message的样子: @范传奇:你好
            if (message.matches("@.+:.+")) { //验证私聊格式是否符合
                //提取对方的昵称
                String toNickname = message.substring(1, message.indexOf(":")); //提取对方的昵称
                if (allOut.containsKey(toNickname)) { //在线
                    PrintWriter pw = allOut.get(toNickname); //获取对方的输出流
                    String toMessage = message.substring(message.indexOf(":") + 1); //截取私聊信息
                    pw.println(nickName + "悄悄对你说:" + toMessage);
                } else { //不在线
                    PrintWriter pw = allOut.get(nickName); //根据昵称获取当前客户端的输出流
                    pw.println("该用户[" + toNickname + "]不在线!");
                }
            } else { //格式不正确, 通知当前客户端格式不对
                PrintWriter pw = allOut.get(nickName); //根据昵称获取当前客户端的输出流
                pw.println("私聊格式不正确!");
            }
        }
    }
}
